Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pydantic Rework #47

Merged
merged 162 commits into from
Aug 23, 2024
Merged

Conversation

multimeric
Copy link
Collaborator

@multimeric multimeric commented Sep 11, 2023

Pydantic

CLI

GUI

  • Rework the GUI to offer the user a sequence of tabs they can sequentially configure
  • Consolidated some repeated GUI elements like the preview button, time range, channel range etc to simplify the codebase

Other

  • Migrate to XArray, which gives us labelled axis (closes Using xarray instead of dask array #41)
  • Created a large number of parameterised tests that capture most combinations of workflow, cropping, deconvolution features, for the Python API, CLI and GUI

Future Work

  • Make save_dir a mandatory parameter, so that the user has to consider the output location. This will involve making the output model optional, and also moving channel_range and time_range into the DeskewParams. This was deprioritised due to the large impact this change would have

@pr4deepr
Copy link
Collaborator

I'm trying the plugin and I get this error when I try to start the plugin in napari:


---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
File ~\Documents\Miniconda3\envs\napari_lattice_test\lib\site-packages\npe2\_command_registry.py:32, in CommandHandler.resolve(self=CommandHandler(id='napari-lattice.dock_widget', ...tice.dock_widget:_napari_lattice_widget_wrapper'))
     31 try:
---> 32     self.function = utils.import_python_name(self.python_name)
        self.function = None
        self.python_name = 'napari_lattice.dock_widget:_napari_lattice_widget_wrapper'
        self = CommandHandler(id='napari-lattice.dock_widget', function=None, python_name='napari_lattice.dock_widget:_napari_lattice_widget_wrapper')
        utils = <module 'npe2.manifest.utils' from 'C:\\Users\\rajasekhar.p\\Documents\\Miniconda3\\envs\\napari_lattice_test\\lib\\site-packages\\npe2\\manifest\\utils.py'>
     33 except Exception as e:

File ~\Documents\Miniconda3\envs\napari_lattice_test\lib\site-packages\npe2\manifest\utils.py:256, in import_python_name(python_name='napari_lattice.dock_widget:_napari_lattice_widget_wrapper')
    254 module_name, funcname = match.groups()  # type: ignore [union-attr]
--> 256 mod = import_module(module_name)
        module_name = 'napari_lattice.dock_widget'
    257 return getattr(mod, funcname)

File ~\Documents\Miniconda3\envs\napari_lattice_test\lib\importlib\__init__.py:126, in import_module(name='napari_lattice.dock_widget', package=None)
    125         level += 1
--> 126 return _bootstrap._gcd_import(name[level:], package, level)
        level = 0
        name = 'napari_lattice.dock_widget'
        name[level:] = 'napari_lattice.dock_widget'
        package = None
        _bootstrap = <module '_frozen_importlib' (frozen)>

File <frozen importlib._bootstrap>:1050, in _gcd_import(name='napari_lattice.dock_widget', package=None, level=0)

File <frozen importlib._bootstrap>:1027, in _find_and_load(name='napari_lattice.dock_widget', import_=<function _gcd_import>)

File <frozen importlib._bootstrap>:1006, in _find_and_load_unlocked(name='napari_lattice.dock_widget', import_=<function _gcd_import>)

File <frozen importlib._bootstrap>:688, in _load_unlocked(spec=ModuleSpec(name='napari_lattice.dock_widget', lo...lattice\\plugin\\napari_lattice\\dock_widget.py'))

File <frozen importlib._bootstrap_external>:883, in exec_module(self=<_frozen_importlib_external.SourceFileLoader object>, module=<module 'napari_lattice.dock_widget' from 'C:\\U...lattice\\plugin\\napari_lattice\\dock_widget.py'>)

File <frozen importlib._bootstrap>:241, in _call_with_frames_removed(f=<built-in function exec>, *args=(<code object <module> at 0x0000022B21959D10, fil...ce\plugin\napari_lattice\dock_widget.py", line 1>, {'Path': <class 'pathlib.Path'>, 'Union': typing.Union, '__builtins__': {'ArithmeticError': <class 'ArithmeticError'>, 'AssertionError': <class 'AssertionError'>, 'AttributeError': <class 'AttributeError'>, 'BaseException': <class 'BaseException'>, 'BlockingIOError': <class 'BlockingIOError'>, 'BrokenPipeError': <class 'BrokenPipeError'>, 'BufferError': <class 'BufferError'>, 'BytesWarning': <class 'BytesWarning'>, 'ChildProcessError': <class 'ChildProcessError'>, 'ConnectionAbortedError': <class 'ConnectionAbortedError'>, ...}, '__cached__': r'C:\Users\rajasekhar.p\test\napari_lattice\plugin...i_lattice\__pycache__\dock_widget.cpython-310.pyc', '__doc__': None, '__file__': r'C:\Users\rajasekhar.p\test\napari_lattice\plugin\napari_lattice\dock_widget.py', '__loader__': <_frozen_importlib_external.SourceFileLoader object>, '__name__': 'napari_lattice.dock_widget', '__package__': 'napari_lattice', '__spec__': ModuleSpec(name='napari_lattice.dock_widget', lo...lattice\\plugin\\napari_lattice\\dock_widget.py'), ...}), **kwds={})

File ~\test\napari_lattice\plugin\napari_lattice\dock_widget.py:7
      6 import numpy as np
----> 7 from lls_core.lattice_data import LatticeData
      8 from lls_core.workflow import import_workflow_modules

File ~\test\napari_lattice\core\lls_core\__init__.py:4
      2 __version__ = "0.2.6"
----> 4 from strenum import StrEnum
      5 from enum import Enum

ModuleNotFoundError: No module named 'strenum'

The above exception was the direct cause of the following exception:.....................
.
.
RuntimeError: Failed to import command at 'napari_lattice.dock_widget:_napari_lattice_widget_wrapper': No module named 'strenum'

@pr4deepr
Copy link
Collaborator

  • GUI:
    Workflow tab: Crop and workflow is not working. It returns a deskewed image without workflow being applied to it.

  • pycudadecon: After installation, I was getting conflicts with numpy (upgraded to 2.0). May need to pin this version to the previous release.

    • rename pyproject.toml key from psf to deconvolution

@pr4deepr
Copy link
Collaborator

pr4deepr commented Aug 14, 2024

deconvolution update:
With pycudadecon, I tried 0.4.0 on Windows, using pip install but was getting an error relating to needing cudadecon as pycudadecon is a python wrapper around it.
Developer recommends conda install as it has precompiled windows and linux binaries, and it works for me too.

But, now I get a different error:

│ C:\Users\Public\Miniconda3\envs\napari_lattice\lib\site-packages\lls_core\deconvolution.py:158   │
│ in pycuda_decon                                                                                  │
│                                                                                                  │
│   155 │   │   psf = np.squeeze(psf)  # remove unit dimensions                                    │
│   156 │   │   assert psf.ndim == 3, f"PSF needs to be 3D. Got {psf.ndim}"                        │
│   157 │   │   # Temporary OTF generation; RLContext ensures memory cleanup (runs rl_init and r   │
│ ❱ 158 │   │   with TemporaryOTF(psf) as otf:                                                     │
│   159 │   │   │   with RLContext(                                                                │
│   160 │   │   │   │   rawdata_shape=image.shape,                                                 │
│   161 │   │   │   │   otfpath=otf.path,                                                          │
│                                                                                                  │
│ ╭─────────────────────────────────────────── locals ───────────────────────────────────────────╮ │
│ │     background = 0                                                                           │ │
│ │       cropping = True                                                                        │ │
│ │         dxdata = 0.14499219272808386                                                         │ │
│ │          dxpsf = 0.14499219272808386                                                         │ │
│ │         dzdata = 0.3                                                                         │ │
│ │          dzpsf = 0.3                                                                         │ │
│ │          image = array([[[ 99,  99,  95, ...,  99, 101,  97],                                │ │
│ │                  │   │   [ 99,  99,  95, ...,  93,  98,  93],                                │ │
│ │                  │   │   [ 99,  99,  97, ...,  99,  97,  94],                                │ │
│ │                  │   │   ...,                                                                │ │
│ │                  │   │   [100, 100,  99, ...,  98,  96,  95],                                │ │
│ │                  │   │   [ 99,  98,  99, ...,  98,  95,  97],                                │ │
│ │                  │   │   [ 97,  97,  96, ...,  96,  96,  96]],                               │ │
│ │                  │                                                                           │ │
│ │                  │      [[ 99,  97,  97, ...,  99,  99,  99],                                │ │
│ │                  │   │   [100,  99,  97, ...,  95, 100,  99],                                │ │
│ │                  │   │   [101,  99,  99, ...,  99,  95, 102],                                │ │
│ │                  │   │   ...,                                                                │ │
│ │                  │   │   [ 97,  97,  97, ...,  98,  97,  96],                                │ │
│ │                  │   │   [ 94,  97,  93, ...,  97,  93,  97],                                │ │
│ │                  │   │   [ 96,  96,  99, ...,  96,  97,  95]],                               │ │
│ │                  │                                                                           │ │
│ │                  │      [[ 99,  99,  98, ...,  99,  99,  97],                                │ │
│ │                  │   │   [ 98, 103,  98, ...,  93, 100,  97],                                │ │
│ │                  │   │   [ 98,  97,  99, ...,  92,  98,  98],                                │ │
│ │                  │   │   ...,                                                                │ │
│ │                  │   │   [ 93, 100,  95, ...,  98,  94,  96],                                │ │
│ │                  │   │   [100,  98, 101, ..., 103,  96,  95],                                │ │
│ │                  │   │   [103,  98,  96, ..., 100,  94,  95]],                               │ │
│ │                  │                                                                           │ │
│ │                  │      ...,                                                                 │ │
│ │                  │                                                                           │ │
│ │                  │      [[ 99, 104,  97, ...,  98,  99,  95],                                │ │
│ │                  │   │   [102, 100,  95, ...,  99,  99,  97],                                │ │
│ │                  │   │   [ 99,  99,  99, ..., 102, 101, 100],                                │ │
│ │                  │   │   ...,                                                                │ │
│ │                  │   │   [ 97,  98,  98, ...,  98,  96,  95],                                │ │
│ │                  │   │   [ 97,  96,  99, ...,  94,  95,  97],                                │ │
│ │                  │   │   [ 98,  95,  92, ...,  99,  93,  98]],                               │ │
│ │                  │                                                                           │ │
│ │                  │      [[ 99,  97,  97, ...,  98,  99,  97],                                │ │
│ │                  │   │   [ 99,  99,  99, ...,  98, 100,  98],                                │ │
│ │                  │   │   [ 99, 101,  95, ..., 100,  99,  98],                                │ │
│ │                  │   │   ...,                                                                │ │
│ │                  │   │   [100,  97,  93, ...,  97,  97,  97],                                │ │
│ │                  │   │   [ 96,  99,  95, ..., 100,  96,  94],                                │ │
│ │                  │   │   [104,  95,  96, ...,  99,  94,  94]],                               │ │
│ │                  │                                                                           │ │
│ │                  │      [[ 99,  99,  98, ...,  98,  99,  98],                                │ │
│ │                  │   │   [ 97,  98,  98, ...,  95,  97, 102],                                │ │
│ │                  │   │   [ 98,  99,  97, ..., 101,  98,  98],                                │ │
│ │                  │   │   ...,                                                                │ │
│ │                  │   │   [ 97,  97, 102, ..., 100,  98,  95],                                │ │
│ │                  │   │   [ 94,  94,  95, ...,  98,  98, 100],                                │ │
│ │                  │   │   [ 98, 100,  94, ...,  97,  94,  95]]], dtype=uint16)                │ │
│ │       num_iter = 10                                                                          │ │
│ │ orig_img_shape = (216, 300, 210)                                                             │ │
│ │       otf_path = None                                                                        │ │
│ │            psf = <xarray.DataArray 'transpose-145d955afd724ac26b38452f33885fcd' (Z: 36, Y:   │ │
│ │                  12,                                                                         │ │
│ │                  │   │   │   │   │   │   │   │   │   │   │   │   │   │   │   │   X: 12)>     │ │
│ │                  dask.array<getitem, shape=(36, 12, 12), dtype=float32, chunksize=(36, 12,   │ │
│ │                  12), chunktype=numpy.ndarray>                                               │ │
│ │                  Coordinates:                                                                │ │
│ │                  │   C        <U11 'Channel:0:0'                                             │ │
│ │                  Dimensions without coordinates: Z, Y, X                                     │ │
│ │                  Attributes:                                                                 │ │
│ │                  │   unprocessed:  {254: <FILETYPE.UNDEFINED: 0>, 256: 12, 257: 12, 258: 32, │ │
│ │                  2...                                                                        │ │
│ │                  │   processed:                                                              │ │
│ │                  ImageJ=1.53t\nimages=36\nslices=36\nunit=micron\nspacing=0....              │ │
│ │       rl_decon = <function rl_decon at 0x0000029CE2750670>                                   │ │
│ │      RLContext = <class 'pycudadecon.deconvolution.RLContext'>                               │ │
│ │   TemporaryOTF = <class 'pycudadecon.otf.TemporaryOTF'>                                      │ │
│ │      x_psf_pad = 6                                                                           │ │
│ │      y_psf_pad = 6                                                                           │ │
│ │      z_psf_pad = 18                                                                          │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
│                                                                                                  │
│ C:\Users\Public\Miniconda3\envs\napari_lattice\lib\site-packages\pycudadecon\otf.py:269 in       │
│ __enter__                                                                                        │
│                                                                                                  │
│   266 │   │   │   elif isinstance(self.psf, str) and os.path.isfile(self.psf):                   │
│   267 │   │   │   │   make_otf(self.psf, self.tempotf.name, **self.kwargs)                       │
│   268 │   │   │   else:                                                                          │
│ ❱ 269 │   │   │   │   raise ValueError(f"Did not expect PSF file as {type(self.psf)}")           │
│   270 │   │   │   self.path = self.tempotf.name                                                  │
│   271 │   │   elif is_otf(self.psf) and os.path.isfile(self.psf):                                │
│   272 │   │   │   self.path = self.psf                                                           │
│                                                                                                  │
│ ╭────────────────────────────── locals ──────────────────────────────╮                           │
│ │ self = <pycudadecon.otf.TemporaryOTF object at 0x0000029CE1B7CC40> │                           │
│ ╰────────────────────────────────────────────────────────────────────╯                           │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
ValueError: Did not expect PSF file as <class 'xarray.core.dataarray.DataArray'>

Looks like we need to convert psf files into numpy arrays..

@multimeric
Copy link
Collaborator Author

multimeric commented Aug 15, 2024

The workflow rework is now in 91c7ea9. I'll document it more thoroughly in the docs PR, but the gist is that we now enforce that the workflow has exactly one parameter called deskewed_image, which will be connected to the output of the deskewing step. Users can also utilise channel, channel_index, roi_index etc if they need the appropriate metadata. Finally, if the workflow has multiple "leaf" (output) tasks, it will be rejected at the validation stage.

Workflow tab: Crop and workflow is not working. It returns a deskewed image without workflow being applied to it.

I couldn't reproduce this, at least after my changes. Did you use the preview or save button?

@multimeric
Copy link
Collaborator Author

Looks like we need to convert psf files into numpy arrays..

This is hard to reproduce because I don't have access to anything cuda compatible myself. Were you running the crop workflow? Because I think I can identify a point where I neglected to convert back to numpy there.

@pr4deepr
Copy link
Collaborator

pr4deepr commented Aug 19, 2024

I'm running into environment configuration issues when I install pycudadecon, even with version 0.4.0. I believe it may be to do with numpy being upgraded to 2.0, and perhaps with napari being configured to 0.5.0. pycudadecon is being installed using conda instead of pip.

@pr4deepr
Copy link
Collaborator

I've figured out the error and I've posted it here: tlambert03/pycudadecon#64

To get around a few dependency errors , I've used following installation commands in python 3.10

restrict napari to 0.4.*
pip install git+https://github.com/multimeric/napari_lattice.git@thin-wrapper#subdirectory=plugin napari==0.4.*

install napari-aiscimageio without updating dependencies (otherwise this caused a lot of issues with scikit-image and tifffile)
pip install napari-aicsimageio --no-deps

pycudadecon
ensure numpy<2, cudadecon is 0.6.*
mamba install -c conda-forge pycudadecon==0.4.* numpy==1.24.* cudadecon==0.6.*

@tlambert03
Copy link

tlambert03 commented Aug 19, 2024

pycudadecon==0.4.*

or pycudadecon v0.5 from pip and you'll be fine. (but the numpy bit is up to you)

@pr4deepr
Copy link
Collaborator

I couldn't reproduce this, at least after my changes. Did you use the preview or save button?

When using Preview, I still get the deskewed image without processing

It works with save...

@pr4deepr
Copy link
Collaborator

pycudadecon
ensure numpy<2, cudadecon is 0.6.*
mamba install -c conda-forge pycudadecon==0.4.* numpy==1.24.* cudadecon==0.6.*

I can confirm that mamba install -c conda-forge pycudadecon numpy==1.24.* works now..

@multimeric
Copy link
Collaborator Author

So to clarify, I'm going to:

  • Pin numpy to < 2. Currently it's unpinned and working fine on the CI, but the CI isn't testing pycudadecon, so this must be related to our own code that uses pycudadecon
  • Pin napari < 0.5. This is probably happening automatically by virtue of our pydantic pin, but I'm happy to be explicit about this
  • Pin pycudadecon >= 0.5
  • Suggest that the user installs cudadecon >= 0.6 in conda (I can't control this using our pyproject.toml).

@multimeric
Copy link
Collaborator Author

When using Preview, I still get the deskewed image without processing

Right, this is a bit tricky. The workflow can produce multiple outputs, including multiple images or no images. Should I add the "first" image output by the workflow, or all of them maybe? The non-image stuff can get thrown away I assume.

@pr4deepr
Copy link
Collaborator

pr4deepr commented Aug 21, 2024

  • Pin numpy to < 2. Currently it's unpinned and working fine on the CI, but the CI isn't testing pycudadecon, so this must be related to our own code that uses pycudadecon

No, not in pyproject.toml. I believe when we run a conda install with pycudadecon, numpy gets upgraded to v2. To prevent this we need to pin it only during pycudadecon installation.

So, command to use for pycudadecon: conda install -c conda-forge pycudadecon numpy==1.24.*

  • Pin napari < 0.5. This is probably happening automatically by virtue of our pydantic pin, but I'm happy to be explicit about this

yes

  • Pin pycudadecon >= 0.5

yes

  • Suggest that the user installs cudadecon >= 0.6 in conda (I can't control this using our pyproject.toml).

No, its working now with pycudadecon 0.5, so we don't need to specify this anymore.

Right, this is a bit tricky. The workflow can produce multiple outputs, including multiple images or no images. Should I add the "first" image output by the workflow, or all of them maybe? The non-image stuff can get thrown away I assume.

We would like only the 'final' image output, if thats what you meant by 'first'..

With non-image stuff, I wanted them to be displayed as a table in napari, but I couldn't really get it working using napari-spreadsheet at this line in napari_lattice here.

Not an immediate priority for now and we could include this as a separate PR..

@multimeric
Copy link
Collaborator Author

When using Preview, I still get the deskewed image without processing

Hopefully fixed in 3ddc78c.

@pr4deepr
Copy link
Collaborator

pr4deepr commented Aug 22, 2024

Hopefully fixed in 3ddc78c.

I'm getting another error now..

This works when I run Save, but when I use Preview, I get the following error with an empty array.

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
File C:\Users\Public\Documents\Miniconda3\envs\napari_lattice_py310\lib\site-packages\napari_lattice\dock_widget.py:164, in LLSZWidget.preview(self=<napari_lattice.dock_widget.LLSZWidget object>, header='', time=0, channel=0)
    161     preview = lattice.process_workflow().extract_preview()
    163 self.parent_viewer.add_image(preview, scale=scale, name="Napari Lattice Preview")
--> 164 max_z = np.argmax(np.sum(preview, axis=(1, 2)))
        preview = <class 'numpy.ndarray'> (150, 224, 207) uint8
        np = <module 'numpy' from 'C:\\Users\\Public\\Documents\\Miniconda3\\envs\\napari_lattice_py310\\lib\\site-packages\\numpy\\__init__.py'>
    165 self.parent_viewer.dims.set_current_step(0, max_z)

File <__array_function__ internals>:180, in sum(*args=(<class 'numpy.ndarray'> (150, 224, 207) uint8,), **kwargs={'axis': (1, 2)})

File C:\Users\Public\Documents\Miniconda3\envs\napari_lattice_py310\lib\site-packages\numpy\core\fromnumeric.py:2296, in sum(a=<class 'numpy.ndarray'> (150, 224, 207) uint8, axis=(1, 2), dtype=None, out=None, keepdims=<no value>, initial=<no value>, where=<no value>)
   2293         return out
   2294     return res
-> 2296 return _wrapreduction(a, np.add, 'sum', axis, dtype, out, keepdims=keepdims,
        np = <module 'numpy' from 'C:\\Users\\Public\\Documents\\Miniconda3\\envs\\napari_lattice_py310\\lib\\site-packages\\numpy\\__init__.py'>
        a = <class 'numpy.ndarray'> (150, 224, 207) uint8
        np.add = <ufunc 'add'>
        axis = (1, 2)
        dtype = None
        out = None
        keepdims = <no value>
        initial = <no value>
        where = <no value>
   2297                       initial=initial, where=where)

File C:\Users\Public\Documents\Miniconda3\envs\napari_lattice_py310\lib\site-packages\numpy\core\fromnumeric.py:84, in _wrapreduction(obj=<class 'numpy.ndarray'> (150, 224, 207) uint8, ufunc=<ufunc 'add'>, method='sum', axis=(1, 2), dtype=None, out=None, **kwargs={'initial': <no value>, 'keepdims': <no value>, 'where': <no value>})
     82             return reduction(axis=axis, dtype=dtype, out=out, **passkwargs)
     83         else:
---> 84             return reduction(axis=axis, out=out, **passkwargs)
        passkwargs = {}
        axis = (1, 2)
        out = None
        reduction = <bound method ArrayOperators.sum of <class 'numpy.ndarray'> (150, 224, 207) uint8>
     86 return ufunc.reduce(obj, axis, dtype, out, **passkwargs)

File C:\Users\Public\Documents\Miniconda3\envs\napari_lattice_py310\lib\site-packages\pyclesperanto_prototype\_tier0\_array_operators.py:89, in ArrayOperators.sum(self=<class 'numpy.ndarray'> (150, 224, 207) uint8, axis=(1, 2), out=None)
     87     result = sum_of_all_pixels(self)
     88 else:
---> 89     raise ValueError("Axis " + axis + " not supported")
        axis = (1, 2)
     90 if out is not None:
     91     np.copyto(out, result.get().astype(out.dtype))

TypeError: can only concatenate str (not "tuple") to str

Workflow I tried:

!!python/object:napari_workflows._workflow.Workflow
_tasks:
  binarisation: !!python/tuple
  - !!python/name:pyclesperanto_prototype.greater_constant ''
  - median
  - null
  - 100
  median: !!python/tuple
  - !!python/name:pyclesperanto_prototype.median_sphere ''
  - deskewed_image
  - null
  - 2
  - 2
  - 2

This workflow should return a binary image.. I was using it in Crop and Preview Workflow.

@multimeric
Copy link
Collaborator Author

multimeric commented Aug 22, 2024

That's weird. It seems to be np.sum is failing with a tuple of axes, but that functionality has been in numpy for ages. Do you have a really old version of numpy?

Oh wait actually it's the pyclesperanto_prototype array that doesn't understand the sum. Can you remind me how you got an array in that format?

@pr4deepr
Copy link
Collaborator

Sorry, which format?
CL array?

@multimeric multimeric mentioned this pull request Aug 22, 2024
Merged
@multimeric
Copy link
Collaborator Author

Yeah that's it. What happened was that your workflow returned a CL array, which doesn't know how to be summed. I've now fixed that with a test.

@pr4deepr
Copy link
Collaborator

Yeah that's it. What happened was that your workflow returned a CL array, which doesn't know how to be summed. I've now fixed that with a test.

Confirm its working!

Copy link
Collaborator

@pr4deepr pr4deepr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All Changes look good and most of core functionality of the plugin and CLI are confirmed to be working...

Great work @multimeric

@pr4deepr pr4deepr merged commit f0e2349 into BioimageAnalysisCoreWEHI:master Aug 23, 2024
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants